using System;
using System.Collections;
using System.Reflection;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels.Http;
using System.Runtime.Remoting.Channels.Ipc;
using System.Net;

using Pegasus.Diagnostics;
using Pegasus.Runtime.Remoting.Channels;

namespace Pegasus.Runtime.Remoting
{
	/// <summary>
	/// The Remote Helper class can be used to setup and shut down the the remoting services
	/// objects.  Users can setup several channels for both server and client side systems.
	/// The client setup only needs to occure if the client will be sinking to events that
	/// are published by the remote server objects.
	/// </summary>
	public static class RemoteHelper
	{
		/// <summary>
		/// Used to setup a listen port for request to remoted objects for a server.
		/// </summary>
		/// <param name="channelType">Type of communication channel</param>
		/// <param name="formatterType">Format to use in passing data</param>
		/// <param name="port">TCP port to listen for new connections on.</param>
		public static string SetupAsServer( RemoteChannelType channelType, RemoteFormatterType formatterType, int port )
		{
			// Check Parameters
			ParamCode.AssertRange( port, 1, IPEndPoint.MaxPort, "port" );

			return SetupAsServer( channelType, formatterType, port, null );
		}

		/// <summary>
		/// Used to setup a listen port for request to remoted objects for a server.
		/// </summary>
		/// <param name="channelType">Type of communication channel</param>
		/// <param name="formatterType">Format to use in passing data</param>
		/// <param name="port">TCP port to listen for new connections on.</param>
		/// <param name="subnets">An array of IP subnets to bind to. Subnet format = 192.168.0.0</param>
		public static string SetupAsServer( RemoteChannelType channelType, RemoteFormatterType formatterType, int port, string[] subnets )
		{
			// Check Parameters
			// channelType is enum
			// formatterType is enum
			ParamCode.AssertRange( port, IPEndPoint.MinPort, IPEndPoint.MaxPort, "port" );
			// subnets can be null

			// Get the valid IP Addresses
			IPAddress approvedAddress = GetValidIPAddress( subnets );

			return SetupAsServer( channelType, formatterType, approvedAddress, port );
		}

		/// <summary>
		/// Used to setup a listen port for request to remoted objects for a server.
		/// </summary>
		/// <param name="channelType">Type of communication channel</param>
		/// <param name="formatterType">Format to use in passing data</param>
		/// <param name="ipAddress">The ip address to bind to.</param>
		/// <param name="port">TCP port to listen for new connections on.</param>
		/// <returns>Return the name of the new channel.</returns>
		public static string SetupAsServer( RemoteChannelType channelType, RemoteFormatterType formatterType, IPAddress ipAddress, int port )
		{
			// Check Parameters
			// channelType is enum
			// formatterType is enum
			// ipAddress can be null
			ParamCode.AssertRange( port, IPEndPoint.MinPort, IPEndPoint.MaxPort, "port" );

			// Get the formmater sink interfaces.
			IClientChannelSinkProvider clientChannelSink;
			IServerChannelSinkProvider serverChannelSink;
			GetFormatterSinks( formatterType, out clientChannelSink, out serverChannelSink );

			string name = string.Format( "Server{0}{1}Address{2}Port{3}", channelType, formatterType, ipAddress, port );
			
			// Setup the property for the channel
            IDictionary properties = new Hashtable();
            properties[ "name" ] = name;
            properties[ "port" ] = port;
            properties[ "setupAsServer" ] = true;

            // Set the port name for IPC channels to the localhost
            properties[ "portName" ] = string.Format( "localhost:{0}", port );

            if( ipAddress != null )
			{
                properties[ "bindTo" ] = ipAddress.ToString();
			}

			// Get the channel
            IChannel channel = GetChannel( channelType, properties, clientChannelSink, serverChannelSink );

			// Register the channel with teh remove services.
			ChannelServices.RegisterChannel( channel, false );

			return name;
		}

		/// <summary>
		/// Call this method to publish a specific type of object from the server.
		/// </summary>
		/// <param name="type">The type of object to be published.</param>
		/// <param name="name">The name of the object to be published.</param>
		/// <param name="mode">The mode (singleton or single call) to process incomming calls.</param>
		public static void RegisterServerType( Type type, string name, WellKnownObjectMode mode )
		{
			// Check Parameters
			ParamCode.AssertNotNull( type, "type" );
			ParamCode.AssertNotEmpty( name, "name" );

			RemotingConfiguration.RegisterWellKnownServiceType( type, name, mode );
		}

        /// <summary>
        /// Used to setup a port for event binding to remoted objects from a server.
        /// </summary>
        /// <remarks>
        /// This method only needs to be called to setup the client side of a remote event.
        /// If the client is not binding a delegate to an event handler on a remote object
        /// then you do not need to call this method.
        /// </remarks>
        /// <param name="channelType">Type of communication channel</param>
        /// <param name="formatterType">Format to use in passing data</param>
        /// <returns>Return the name of the new channel.</returns>
        public static string SetupAsClient( RemoteChannelType channelType, RemoteFormatterType formatterType )
        {
            return SetupAsClient( channelType, formatterType, 0 );
        }

        /// <summary>
		/// Used to setup a port for event binding to remoted objects from a server.
		/// </summary>
		/// <remarks>
		/// This method only needs to be called to setup the client side of a remote event.
		/// If the client is not binding a delegate to an event handler on a remote object
		/// then you do not need to call this method.
		/// </remarks>
		/// <param name="channelType">Type of communication channel</param>
		/// <param name="formatterType">Format to use in passing data</param>
		/// <param name="port">TCP port for incomming events, if zero (0) a dynamic port is selected.</param>
		/// <returns>Return the name of the new channel.</returns>
		public static string SetupAsClient( RemoteChannelType channelType, RemoteFormatterType formatterType, int port )
		{
			// Check Parameters
			ParamCode.AssertRange( port, IPEndPoint.MinPort, IPEndPoint.MaxPort, "port" );

			// Get the formmater sink interfaces.
			IClientChannelSinkProvider clientChannelSink;
			IServerChannelSinkProvider serverChannelSink;
			GetFormatterSinks( formatterType, out clientChannelSink, out serverChannelSink );

			string name = string.Format( "Client{0}{1}Port{2}", channelType, formatterType, port );

			// Setup the property for the channel
            IDictionary properties = new Hashtable();
            properties[ "name" ] = name;
            properties[ "port" ] = port;
            properties[ "setupAsServer" ] = false;

			// Get the channel
            IChannel channel = GetChannel( channelType, properties, clientChannelSink, serverChannelSink );

			// Register the channel with teh remove services.
			ChannelServices.RegisterChannel( channel, false );

			return name;
		}

		/// <summary>
		/// Close ALL open server and/or client channels and unregisters all remoted objects
		/// </summary>
		public static void ShutDownRemoting()
		{
			IChannel[] channels = ChannelServices.RegisteredChannels;
			if( channels != null )
			{
				foreach( IChannel channel in channels )
				{
					ChannelServices.UnregisterChannel( channel );
				}
			}
		}

		/// <summary>
		/// Closes the given channel.
		/// </summary>
		/// <param name="name">The name.</param>
		public static void ShutDownChannel( string name )
		{
			IChannel[] channels = ChannelServices.RegisteredChannels;
			if( channels != null )
			{
				foreach( IChannel channel in channels )
				{
					if( channel.ChannelName == name )
					{
						ChannelServices.UnregisterChannel( channel );
					}
				}
			}
		}

		/// <summary>
		/// Get a reference to a remote object from a server.
		/// </summary>
		/// <param name="channelType">Type of communication channel</param>
		/// <param name="server">Name or IP address of the server</param>
		/// <param name="port">TCP port which the server is listening on</param>
		/// <param name="name">The name that the object was published under.</param>
		/// <returns>A reference to the remote object</returns>
		public static object GetRemoteObject( RemoteChannelType channelType, string server, int port, string name )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( server, "server" );
			ParamCode.AssertRange( port, IPEndPoint.MinPort, IPEndPoint.MaxPort, "port" );
			ParamCode.AssertNotEmpty( name, "name" );

			string url = "";
			switch( channelType )
			{
				case RemoteChannelType.Tcp:
					url = string.Format( "tcp://{0}:{1}/{2}", server, port, name );
					break;

				case RemoteChannelType.Http:
					url = string.Format( "http://{0}:{1}/{2}", server, port, name );
					break;

                case RemoteChannelType.Ipc:
                    url = string.Format( "ipc://{0}:{1}/{2}", server, port, name );
                    break;
                
                default:
					throw new ArgumentException( "Unknown channel type", "channelType" );
			}

			// Use the base class MarshalByRefObject and then let the application down cast it.
			return Activator.GetObject( typeof( MarshalByRefObject ), url );
		}

		/// <summary>
		/// Gets the server sinks interface for the specified formatter.
		/// </summary>
		/// <param name="formatterType">Type of the formatter.</param>
		/// <param name="clientChannelSink">The client channel sink.</param>
		/// <param name="serverChannelSink">The server channel sink.</param>
		private static void GetFormatterSinks( RemoteFormatterType formatterType, out IClientChannelSinkProvider clientChannelSink, out IServerChannelSinkProvider serverChannelSink )
		{
			clientChannelSink = null;
			serverChannelSink = null;

			switch( formatterType )
			{
				case RemoteFormatterType.Binary:
					clientChannelSink = new BinaryClientFormatterSinkProvider();
					serverChannelSink = new BinaryServerFormatterSinkProvider();
					( (BinaryServerFormatterSinkProvider) serverChannelSink ).TypeFilterLevel = TypeFilterLevel.Full;
					break;

				case RemoteFormatterType.Soap:
					clientChannelSink = new SoapClientFormatterSinkProvider();
					serverChannelSink = new SoapServerFormatterSinkProvider();
					( (SoapServerFormatterSinkProvider) serverChannelSink ).TypeFilterLevel = TypeFilterLevel.Full;
					break;

                case RemoteFormatterType.PegasusXml:
                    clientChannelSink = new XmlClientFormatterSinkProvider();
                    serverChannelSink = new XmlServerFormatterSinkProvider();
                    break;
                    
				default:
					throw new ArgumentException( "Unknown formatter type", "formatterType" );
			}
		}

		/// <summary>
		/// Gets the channel.
		/// </summary>
		/// <param name="channelType">Type of the channel.</param>
		/// <param name="properties">The properties.</param>
		/// <param name="clientChannelSink">The client channel sink.</param>
		/// <param name="serverChannelSink">The server channel sink.</param>
		/// <returns></returns>
		private static IChannel GetChannel( RemoteChannelType channelType, IDictionary properties, IClientChannelSinkProvider clientChannelSink, IServerChannelSinkProvider serverChannelSink )
		{
			IChannel channel = null;

			switch( channelType )
			{
				case RemoteChannelType.Tcp:
					channel = new TcpChannel( properties, clientChannelSink, serverChannelSink );
					break;

				case RemoteChannelType.Http:
					channel = new HttpChannel( properties, clientChannelSink, serverChannelSink );
					break;

                case RemoteChannelType.Ipc:
                    channel = new IpcChannel( properties, clientChannelSink, serverChannelSink );
                    break;

				default:
					throw new ArgumentException( "Unknown channel type", "channelType" );
			}

			return channel;
		}

		/// <summary>
		/// This will return an ArrayList of IP addresses from the local machine that are 
		/// valid for the set of subnets that have been passed in.
		/// </summary>
		/// <param name="subnets">An array of subnets that are valid</param>
		/// <returns>A list of IP addresses that are valid for the subnets passed in</returns>
		private static IPAddress GetValidIPAddress( string[] subnets )
		{
			if( subnets == null )
			{
				return null;
			}

			byte[] ipBytes;
			byte[] subnetBytes;
			IPAddress approvedAddress = IPAddress.Loopback;
			IPAddress[] localAddresses = Dns.GetHostEntry( Environment.MachineName ).AddressList;

			foreach( string subnet in subnets )
			{
				foreach( IPAddress ip in localAddresses )
				{
					ipBytes = ip.GetAddressBytes();
					subnetBytes = IPAddress.Parse( subnet ).GetAddressBytes();
					if( ipBytes.Length == subnetBytes.Length )
					{
						bool isValid = true;
						for( int i = 0; i < ipBytes.Length; i++ )
						{
							int tmpByte = ( ipBytes[ i ] | subnetBytes[ i ] );
							isValid = ( isValid && ipBytes[ i ] == tmpByte );
						}
						if( isValid )
							return ip;
					}
				}
			}

			return approvedAddress;
		}
	}
}
